readme_Obsidian_to_Facebook_012826
Obsidian에서 작성한 노트를 Python파일을 통해서 Facebook에 자동으로 옮기는 방법
1 단계
기본 페키지
pip install selenium
pip install webdriver-manager
pip install python-frontmatter
pip install pillow # 선택적 (마크다운 처리)
pip install markdown pip install beautifulsoup4
2 단계
# Obsidian parser
#!/usr/bin/env python3
"""
Obsidian 노트를 Facebook 게시물로 변환
"""
import frontmatter
import re
from pathlib import Path
from typing import Dict, List, Optional
class ObsidianToFacebook:
"""Obsidian 마크다운을 Facebook 형식으로 변환"""
def __init__(self, vault_path: str):
"""
Args:
vault_path: Obsidian vault 경로
"""
self.vault_path = Path(vault_path)
def parse_note(self, note_path: str) -> Dict:
"""
Obsidian 노트 파싱
Args:
note_path: 노트 파일 경로
Returns:
{
'title': 제목,
'content': 본문 (Facebook 형식),
'images': 이미지 경로 리스트,
'metadata': frontmatter 메타데이터,
'tags': 태그 리스트
}
"""
note_path = Path(note_path)
with open(note_path, 'r', encoding='utf-8') as f:
post = frontmatter.load(f)
metadata = post.metadata
content = post.content
# 이미지 추출
images = self._extract_images(content, note_path.parent)
# 마크다운을 Facebook 텍스트로 변환
fb_content = self._markdown_to_facebook(content)
# 태그 추출
tags = self._extract_tags(content, metadata)
# 제목 추출 (frontmatter 또는 첫 번째 헤딩)
title = metadata.get('title', self._extract_title(content))
return {
'title': title,
'content': fb_content,
'images': images,
'metadata': metadata,
'tags': tags
}
def _extract_images(self, content: str, base_path: Path) -> List[str]:
"""
마크다운에서 이미지 경로 추출
Args:
content: 마크다운 내용
base_path: 노트의 기본 경로
Returns:
이미지 파일 전체 경로 리스트
"""
images = []
# Obsidian 형식: ![[image.png]]
obsidian_pattern = r'!\[\[([^\]]+\.(jpg|jpeg|png|gif|webp))\]\]'
obsidian_matches = re.findall(obsidian_pattern, content, re.IGNORECASE)
# 표준 마크다운 형식: 
markdown_pattern = r'!\[.*?\]\(([^\)]+\.(jpg|jpeg|png|gif|webp))\)'
markdown_matches = re.findall(markdown_pattern, content, re.IGNORECASE)
# Obsidian 형식 처리
for match in obsidian_matches:
image_name = match[0]
image_path = self._find_image_in_vault(image_name)
if image_path:
images.append(str(image_path))
# 표준 마크다운 형식 처리
for match in markdown_matches:
image_path_str = match[0]
# 절대 경로인 경우
if Path(image_path_str).is_absolute():
if Path(image_path_str).exists():
images.append(image_path_str)
else:
# 상대 경로인 경우
full_path = base_path / image_path_str
if full_path.exists():
images.append(str(full_path.resolve()))
else:
# vault에서 검색
found_path = self._find_image_in_vault(image_path_str)
if found_path:
images.append(str(found_path))
return images
def _find_image_in_vault(self, image_name: str) -> Optional[Path]:
"""
Vault 전체에서 이미지 파일 검색
Args:
image_name: 이미지 파일명
Returns:
찾은 이미지의 전체 경로 또는 None
"""
# Attachments 폴더 우선 검색
attachments_dir = self.vault_path / "Attachments"
if attachments_dir.exists():
image_path = attachments_dir / image_name
if image_path.exists():
return image_path
# 전체 vault 검색
for image_path in self.vault_path.rglob(image_name):
if image_path.is_file():
return image_path
return None
def _markdown_to_facebook(self, content: str) -> str:
"""
마크다운을 Facebook 텍스트로 변환
Args:
content: 마크다운 내용
Returns:
Facebook 형식 텍스트
"""
# 이미지 링크 제거
content = re.sub(r'!\[\[([^\]]+)\]\]', '', content)
content = re.sub(r'!\[.*?\]\([^\)]+\)', '', content)
# Obsidian 위키링크 변환: [[링크]] → 링크
content = re.sub(r'\[\[([^\]|]+)(\|[^\]]+)?\]\]', r'\1', content)
# 표준 마크다운 링크 변환: [텍스트](URL) → 텍스트 (URL)
content = re.sub(r'\[([^\]]+)\]\(([^\)]+)\)', r'\1 (\2)', content)
# 볼드 처리: **텍스트** → 텍스트 (Facebook은 자동 볼드 지원 안 함)
content = re.sub(r'\*\*([^\*]+)\*\*', r'\1', content)
content = re.sub(r'__([^_]+)__', r'\1', content)
# 이탤릭 처리: *텍스트* → 텍스트
content = re.sub(r'\*([^\*]+)\*', r'\1', content)
content = re.sub(r'_([^_]+)_', r'\1', content)
# 코드 블록 제거
content = re.sub(r'```[\s\S]*?```', '', content)
content = re.sub(r'`([^`]+)`', r'\1', content)
# 헤딩 변환: # 제목 → 제목 (줄바꿈 추가)
content = re.sub(r'^#{1,6}\s+(.+)
# 🤖 3단계: Facebook 자동 게시 (Selenium)
```python
#facebook selenium poster
#!/usr/bin/env python3
"""
Selenium을 사용한 Facebook 자동 게시
API 없이 브라우저 자동화로 게시
"""
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from webdriver_manager.chrome import ChromeDriverManager
import time
from pathlib import Path
from typing import List, Optional
import json
class FacebookPoster:
"""Selenium을 사용한 Facebook 자동 게시"""
def __init__(self, headless: bool = False, profile_path: Optional[str] = None):
"""
Args:
headless: 브라우저 숨기기 (False = 브라우저 보임)
profile_path: Chrome 프로필 경로 (로그인 유지용)
"""
self.headless = headless
self.profile_path = profile_path
self.driver = None
def start_browser(self):
"""브라우저 시작"""
chrome_options = Options()
# 프로필 경로 설정 (로그인 유지)
if self.profile_path:
chrome_options.add_argument(f"user-data-dir={self.profile_path}")
# 헤드리스 모드
if self.headless:
chrome_options.add_argument("--headless=new")
# 기타 옵션
chrome_options.add_argument("--disable-blink-features=AutomationControlled")
chrome_options.add_argument("--disable-dev-shm-usage")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--window-size=1920,1080")
# User Agent (봇 감지 방지)
chrome_options.add_argument(
"user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36"
)
# 드라이버 시작
service = Service(ChromeDriverManager().install())
self.driver = webdriver.Chrome(service=service, options=chrome_options)
print("✅ 브라우저 시작됨")
def login(self, email: str, password: str, save_session: bool = True):
"""
Facebook 로그인
Args:
email: Facebook 이메일
password: Facebook 비밀번호
save_session: 세션 저장 여부
"""
print("🔐 Facebook 로그인 중...")
self.driver.get("https://www.facebook.com/")
time.sleep(3)
try:
# 이메일 입력
email_input = WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.ID, "email"))
)
email_input.send_keys(email)
# 비밀번호 입력
password_input = self.driver.find_element(By.ID, "pass")
password_input.send_keys(password)
# 로그인 버튼 클릭
login_button = self.driver.find_element(By.NAME, "login")
login_button.click()
print("⏳ 로그인 처리 중...")
time.sleep(5)
# 로그인 성공 확인
if "login" not in self.driver.current_url.lower():
print("✅ 로그인 성공!")
return True
else:
print("❌ 로그인 실패")
return False
except Exception as e:
print(f"❌ 로그인 오류: {e}")
return False
def is_logged_in(self) -> bool:
"""로그인 상태 확인"""
try:
self.driver.get("https://www.facebook.com/")
time.sleep(3)
# 로그인되어 있으면 뉴스피드가 보임
return "login" not in self.driver.current_url.lower()
except:
return False
def post_to_profile(self, text: str, images: List[str] = None):
"""
개인 프로필에 게시
Args:
text: 게시물 텍스트
images: 이미지 파일 경로 리스트
"""
print("\n📝 게시물 작성 중...")
try:
# 개인 프로필로 이동
self.driver.get("https://www.facebook.com/me")
time.sleep(3)
# "무슨 생각을 하고 계신가요?" 클릭
# 여러 가능한 선택자 시도
click_selectors = [
"//span[contains(text(), '무슨 생각')]",
"//span[contains(text(), 'What')]",
"//div[@role='button' and contains(@aria-label, 'Create')]",
"//div[contains(@class, 'x1i10hfl')]//span"
]
create_post_clicked = False
for selector in click_selectors:
try:
create_button = WebDriverWait(self.driver, 10).until(
EC.element_to_be_clickable((By.XPATH, selector))
)
create_button.click()
create_post_clicked = True
print("✅ 게시물 작성 창 열림")
break
except:
continue
if not create_post_clicked:
print("❌ 게시물 작성 버튼을 찾을 수 없습니다")
return False
time.sleep(2)
# 텍스트 입력
# contenteditable div 찾기
text_selectors = [
"//div[@contenteditable='true'][@role='textbox']",
"//div[@contenteditable='true' and @aria-label]",
"//div[contains(@class, 'notranslate')][@contenteditable='true']"
]
text_entered = False
for selector in text_selectors:
try:
text_box = WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.XPATH, selector))
)
text_box.click()
time.sleep(1)
text_box.send_keys(text)
text_entered = True
print("✅ 텍스트 입력 완료")
break
except:
continue
if not text_entered:
print("❌ 텍스트 입력 실패")
return False
time.sleep(2)
# 이미지 업로드
if images:
print(f"📷 이미지 {len(images)}개 업로드 중...")
# 사진/동영상 버튼 찾기
photo_selectors = [
"//div[@aria-label='사진/동영상']",
"//div[@aria-label='Photo/video']",
"//input[@type='file' and @accept]"
]
for selector in photo_selectors:
try:
if selector.startswith("//input"):
# 파일 입력 직접 찾기
file_input = self.driver.find_element(By.XPATH, selector)
else:
# 버튼 클릭 후 파일 입력 찾기
photo_button = WebDriverWait(self.driver, 10).until(
EC.element_to_be_clickable((By.XPATH, selector))
)
photo_button.click()
time.sleep(1)
file_input = self.driver.find_element(By.XPATH, "//input[@type='file']")
# 모든 이미지 경로를 한 번에 입력
all_images = "\n".join([str(Path(img).resolve()) for img in images])
file_input.send_keys(all_images)
print(f"✅ 이미지 업로드 완료")
time.sleep(3) # 업로드 대기
break
except Exception as e:
print(f"⚠️ 이미지 업로드 시도 실패: {e}")
continue
# 게시 버튼 클릭
post_selectors = [
"//div[@aria-label='게시'][@role='button']",
"//div[@aria-label='Post'][@role='button']",
"//span[text()='게시']",
"//span[text()='Post']"
]
for selector in post_selectors:
try:
post_button = WebDriverWait(self.driver, 10).until(
EC.element_to_be_clickable((By.XPATH, selector))
)
post_button.click()
print("✅ 게시 버튼 클릭!")
break
except:
continue
# 게시 완료 대기
time.sleep(5)
print("🎉 게시물이 성공적으로 게시되었습니다!")
return True
except Exception as e:
print(f"❌ 게시 오류: {e}")
import traceback
traceback.print_exc()
return False
def post_to_page(self, page_url: str, text: str, images: List[str] = None):
"""
페이지에 게시
Args:
page_url: 페이지 URL (예: https://www.facebook.com/KAMCABQ/)
text: 게시물 텍스트
images: 이미지 파일 경로 리스트
"""
print(f"\n📝 페이지 게시 중: {page_url}")
try:
# 페이지로 이동
self.driver.get(page_url)
time.sleep(3)
# 이후 로직은 post_to_profile과 유사
# (페이지 게시는 프로필 게시와 거의 동일)
return self.post_to_profile(text, images)
except Exception as e:
print(f"❌ 페이지 게시 오류: {e}")
return False
def close(self):
"""브라우저 종료"""
if self.driver:
self.driver.quit()
print("✅ 브라우저 종료됨")
# 사용 예제
if __name__ == "__main__":
# 설정
FACEBOOK_EMAIL = "your_email@example.com"
FACEBOOK_PASSWORD = "your_password"
# Chrome 프로필 경로 (선택사항 - 로그인 유지용)
# Windows: C:/Users/YourName/AppData/Local/Google/Chrome/User Data
# Mac: ~/Library/Application Support/Google/Chrome
CHROME_PROFILE = None # 또는 실제 경로
# 브라우저 시작
poster = FacebookPoster(headless=False, profile_path=CHROME_PROFILE)
poster.start_browser()
try:
# 로그인 확인
if not poster.is_logged_in():
print("로그인 필요...")
poster.login(FACEBOOK_EMAIL, FACEBOOK_PASSWORD)
else:
print("이미 로그인되어 있습니다!")
# 게시물 작성
text = """
📌 테스트 게시물
이것은 Obsidian에서 자동으로 게시된 테스트 메시지입니다.
#테스트 #자동화 #Python
"""
# 이미지 (선택사항)
images = [
# "/path/to/image1.jpg",
# "/path/to/image2.png"
]
# 개인 프로필에 게시
poster.post_to_profile(text, images)
# 또는 페이지에 게시
# poster.post_to_page("https://www.facebook.com/KAMCABQ/", text, images)
print("\n✅ 완료!")
except Exception as e:
print(f"❌ 오류 발생: {e}")
import traceback
traceback.print_exc()
finally:
input("\n계속하려면 Enter를 누르세요...")
poster.close()
```, r'\1\n', content, flags=re.MULTILINE)
# 리스트 정리
content = re.sub(r'^\s*[-*+]\s+', '• ', content, flags=re.MULTILINE)
content = re.sub(r'^\s*\d+\.\s+', '', content, flags=re.MULTILINE)
# 인용구 정리: > 텍스트 → 텍스트
content = re.sub(r'^>\s+', '', content, flags=re.MULTILINE)
# 수평선 제거
content = re.sub(r'^[-*_]{3,}
# 🤖 3단계: Facebook 자동 게시 (Selenium)
{{CODE_BLOCK_3}}, '', content, flags=re.MULTILINE)
# Obsidian 메타데이터 제거 (::)
content = re.sub(r'^[^:]+::\s*.+
# 🤖 3단계: Facebook 자동 게시 (Selenium)
{{CODE_BLOCK_3}}, '', content, flags=re.MULTILINE)
# 연속된 빈 줄을 하나로
content = re.sub(r'\n{3,}', '\n\n', content)
# 앞뒤 공백 제거
content = content.strip()
return content
def _extract_tags(self, content: str, metadata: Dict) -> List[str]:
"""
태그 추출
Args:
content: 마크다운 내용
metadata: frontmatter 메타데이터
Returns:
태그 리스트 (# 포함)
"""
tags = []
# frontmatter에서 태그 추출
if 'tags' in metadata:
fm_tags = metadata['tags']
if isinstance(fm_tags, list):
tags.extend([f"#{tag.strip('#')}" for tag in fm_tags])
elif isinstance(fm_tags, str):
tags.append(f"#{fm_tags.strip('#')}")
# 본문에서 해시태그 추출
hashtag_pattern = r'#[가-힣a-zA-Z0-9_]+'
content_tags = re.findall(hashtag_pattern, content)
tags.extend(content_tags)
# 중복 제거
tags = list(set(tags))
return tags
def _extract_title(self, content: str) -> str:
"""
본문에서 제목 추출 (첫 번째 # 헤딩)
Args:
content: 마크다운 내용
Returns:
제목 또는 빈 문자열
"""
heading_match = re.search(r'^#\s+(.+)
# 🤖 3단계: Facebook 자동 게시 (Selenium)
{{CODE_BLOCK_3}}, content, re.MULTILINE)
if heading_match:
return heading_match.group(1).strip()
return ''
def format_for_facebook(self, parsed_data: Dict,
include_title: bool = True,
include_tags: bool = True) -> str:
"""
Facebook 게시물 최종 포맷팅
Args:
parsed_data: parse_note() 결과
include_title: 제목 포함 여부
include_tags: 태그 포함 여부
Returns:
Facebook 게시물 텍스트
"""
parts = []
# 제목 추가
if include_title and parsed_data['title']:
parts.append(f"📌 {parsed_data['title']}")
parts.append("") # 빈 줄
# 본문 추가
if parsed_data['content']:
parts.append(parsed_data['content'])
parts.append("") # 빈 줄
# 태그 추가
if include_tags and parsed_data['tags']:
tags_str = " ".join(parsed_data['tags'])
parts.append(tags_str)
return "\n".join(parts).strip()
# 사용 예제
if __name__ == "__main__":
# 테스트
vault_path = "/path/to/your/obsidian/vault"
parser = ObsidianToFacebook(vault_path)
note_path = vault_path + "/test-note.md"
# 노트 파싱
result = parser.parse_note(note_path)
print("=" * 70)
print("파싱 결과")
print("=" * 70)
print(f"\n제목: {result['title']}")
print(f"\n이미지: {result['images']}")
print(f"\n태그: {result['tags']}")
print(f"\n내용:\n{result['content']}")
# Facebook 형식으로 포맷팅
fb_post = parser.format_for_facebook(result)
print("\n" + "=" * 70)
print("Facebook 게시물 미리보기")
print("=" * 70)
print(fb_post)
🤖 3단계: Facebook 자동 게시 (Selenium)
{{CODE_BLOCK_3}}